/*
 * webcmd.cpp
 *
 *  Created on: 15-May-2011
 *      Author: tan-ce
 */

#include <cstdlib>
#include <iostream>
#include <sstream>
#include <fstream>
#include <vector>
#include <algorithm>
#include <unistd.h>
#include <poll.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/resource.h>
#include <dirent.h>
#include "tcetools/socket.h"
#include "tcetools/basic_http.h"
#include "tcetools/proc_mgmt.h"

#include "constants.h"
#include "webcmd.h"
#include "indexed_log.h"

using namespace std;
using namespace tcetools;

extern char _binary_html_index_html_start;
extern char _binary_html_index_html_end;
extern char _binary_html_jquery_js_start;
extern char _binary_html_jquery_js_end;

void http_server_loop();
void process_http_cmd(char cmd, ChildProcess *http_server, ChildProcess *mc_server);
void minecraft_server_loop(int argc, char *argv[], ChildProcess &http_server);
void log_stdout(Socket &cur_stdout, int real_stdout);

std::string baseurl;

bool start_serv = false;
bool start_backup = false;
bool terminating = false;

IndexedLog *chat_log;
IndexedLog *sys_log;

std::string inline get_timestamp() {
    time_t now;
    time(&now);
    struct tm *tminfo = localtime(&now);
    char time_buf[15];
    strftime(time_buf, 14, "%Y%m%d-%H%M", tminfo);
    return string(time_buf);
}

// Spawn our helper processes here, before memory is allocated for stuff
// needed in minecraft_server_loop
void launch_webcmd(int argc, char *argv[]) {
	// Create the HTTP server process
	ChildProcess http_server;
	if (http_server.is_child()) {
		http_server_loop();
		_exit(0);
	}

	chat_log = new IndexedLog("chat");
	sys_log = new IndexedLog("sys");

	minecraft_server_loop(argc, argv, http_server);

	delete chat_log;
	delete sys_log;
}

void log_stdout(Socket &cur_stdout, int real_stdout) {
	string curline, toecho;
	bool to_sys_log = true;

	while(cur_stdout.can_read()) {
		try {
			curline = cur_stdout.read_line(true);
		} catch (SocketError &se) {
			if (se.would_block()) return;
			throw se;
		}

		string prefix;
		switch (curline.c_str()[0]) {
		case 'm': {// Minecraft server
			int pos;
			string serv_msg;

			pos = curline.find("[INFO] ");
			if (pos == string::npos) {
				pos = curline.find("[WARNING] ");
				if (pos == string::npos) pos = 1;
			} else {
				// See if it's a chat message
				int chatpos = curline.find("[INFO] <");
				if (    chatpos != string::npos &&
						(chatpos = curline.find(">")) != string::npos) {

					string name = curline.substr(pos + 8, chatpos - pos - 8);

					chatpos += 2;

					string chatmsg = string("[") + name + string("] ") +
							curline.substr(chatpos, curline.length() - chatpos);

					chat_log->append_log(chatmsg);
					to_sys_log = false;

					toecho = string("chat > ") + chatmsg;
					break;
				}
				chatpos = curline.find("[INFO] [CONSOLE] ");
				if (chatpos != string::npos) {
					string chatmsg = curline.substr(chatpos + 17, curline.length() - chatpos - 17);

					chat_log->append_log(chatmsg);
					to_sys_log = false;

					toecho = string("chat > ") + chatmsg;
					break;
				}
			}

			serv_msg = curline.substr(pos, curline.length() - pos);

			toecho = string("sv > ") + serv_msg;

			break;
		}
        case 'c': // Direct to chat log
            chat_log->append_log(curline.c_str() + 1);
            break;

		case 'p': // Proxy messages
			toecho = string("proxy > ") + (curline.c_str() + 1);
			break;
		case 'h': // HTTP server messages
			toecho = string("websv > ") + (curline.c_str() + 1);
			break;

		default:
			toecho = curline;
		}

		// Log to sys log
		if (to_sys_log) sys_log->append_log(toecho);

		// Echo to real stdout so we can see stuff on the console
		write(real_stdout, toecho.c_str(), toecho.length());
		write(real_stdout, "\n", 1);
	}
}

void exec_minecraft_server(char *argv[]) {
	if (setpriority(PRIO_PROCESS, 0, -5)) {
		cout << "Attempted to set Minecraft Server process priority to -5, but failed." << endl;
	} else {
		cout << "Minecraft Server process priority set to -5" << endl;

		uid_t real_id = getuid();

		if (real_id == 0) {
			cout << "Current effective user is root, and the real ID is also root. "
					"It is more secure to chown the binary to root, and flip the "
					"setuid bit. Then I can change uid to the real (unprivilaged) "
					"user." << endl;
		} else {
			cout << "Current effective user is root. Changing effective uid to "
					<< real_id << "." << endl;
			seteuid(real_id);
		}
	}

	cout << "Starting Minecraft Server..." << endl;
	ChildProcess::exec_cmd(argv[0], argv);
}

void run_backup() {
	// First, let's figure out what the world is called
	string world_name;
	ifstream server_prop("server.properties");
	if (server_prop.is_open()) {
		string line;
		while(server_prop.good()) {
			getline(server_prop, line);
			int pos = line.find("level-name=");
			if (pos != string::npos) {
				world_name = line.substr(pos + 11, line.length() - 11 - pos);
				break;
			}
		}
		server_prop.close();
	} else {
		cout << "pCannot open server.properties. Backup aborted." << endl;
		return;
	}

	if (world_name.length() == 0) {
		cout << "pCannot determine world name. Backup aborted." << endl;
		return;
	}

	cout << "pWorld to backup: " << world_name << endl;

	// Check backup directory
	struct stat st;
	if(stat("backups",&st)) {
		cout << "pBackup directory doesn't seem to exist. Attempting to create." << endl;
		if (mkdir("backups", 0777)) {
			cout << "pFailed to create backup directory. Aborting." << endl;
			return;
		}
	}

	cout << "pRunning commands to backup world..." << endl;

	string cmd;

	cmd = "rm -fr backups/most_recent";
	cout << "p" << cmd << endl;
	system(cmd.c_str());

	cmd = "cp -r '" + world_name + "' backups/most_recent";
	cout << "p" << cmd << endl;
	system(cmd.c_str());

	cmd = "tar -caf backups/" + get_timestamp() + ".tar.bz2 backups/most_recent";
	cout << "p" << cmd << endl;
	system(cmd.c_str());

	cmd = "chown -R minecraft backups/" + get_timestamp() + ".tar.bz2 backups/most_recent";
	cout << "p" << cmd << endl;
	system(cmd.c_str());
}

void minecraft_server_loop(int argc, char *argv[], ChildProcess &http_server) {
	string server_cmd;
	for (int i = 0; i < argc; i++) {
		server_cmd.append(string(argv[i]) + " ");
	}

	cout << "Command to launch server is set to:\n"
		 << server_cmd << endl;

	// Redirect stdout so we can log everything nicely
	int new_stdout = dup(STDOUT_FILENO);
	int fake_stdout[2];
	pipe(fake_stdout);
	Socket cur_stdout(fake_stdout[0]);
	cur_stdout.set_noblock();
	cur_stdout.set_line_delimiter("\n");
	dup2(fake_stdout[1], STDOUT_FILENO);

	ChildProcess *mc_server = 0;

	// try {
		int closed = 1 | 2;
		struct pollfd topoll[4];
		for (int i = 0; i < 4; i++) {
			topoll[i].events = POLLIN;
		}
		do {
			// Wait for I/O or until 500ms
			topoll[0].fd = http_server.out().get_fd();
			topoll[1].fd = http_server.err().get_fd();

			if (mc_server) {
				topoll[2].fd = mc_server->out().get_fd();
				topoll[3].fd = mc_server->err().get_fd();
				poll(topoll, 4, 1000);
			} else poll(topoll, 2, 1000);

			////////////////////////////////////
			// Read server's output if available
			if (mc_server) {
				try {
					cout << "m" << mc_server->out().read_line(true) << endl;
				} catch (SocketError &se) {
					if (se.sck_closed()) closed |= 1;
					else if (!se.would_block()) throw se;
				}

				try {
					cout << "m" << mc_server->err().read_line(true) << endl;
				} catch (SocketError &se) {
					if (se.sck_closed()) closed |= 2;
					else if (!se.would_block()) throw se;
				}
			}
			//
			////////////////////////////////////

			////////////////////////////////////
			// Minecraft server is dead
			if (closed == (1 | 2)) {
				if (mc_server) {
					cout << "pServer process not running" << endl;
					delete mc_server;
					mc_server = 0;
				}

				if (terminating) {
					cout << "pShutting down proxy" << endl;
					log_stdout(cur_stdout, new_stdout);
					return;
				}

				// Do backup if requested
				if (start_backup) {
					start_backup = false;
					run_backup();
					start_serv = true;
				}

				// Start it if requested
				if (start_serv) {
					closed = 0;
					start_serv = false;
					mc_server = new ChildProcess();

					if (mc_server->is_parent()) {
						cout << "pMinecraft server process launched with PID " << mc_server->get_pid() << endl;
					} else exec_minecraft_server(argv);
				}
			}
			//
			////////////////////////////////////

			////////////////////////////////////
			// See if the Http component sent us any messages
			try {
				string HttpCmd = http_server.out().read_line(true);
				//cout << "pRecv'd from HTTP module: " << HttpCmd << endl;
				process_http_cmd(HttpCmd.c_str()[0], &http_server, mc_server);
			} catch (SocketError &se) {
				if (se.sck_closed()) cout << "pHTTP server terminated prematurely" << endl;
				else if (!se.would_block()) throw se;
			}

			// Read unexpected errors from the Http Component
			try {
				cout << "h[stderr] " << http_server.err().read_line(true) << endl;
			} catch (SocketError &se) {
				if (se.sck_closed()) throw ProcessError("HTTP server terminated prematurely");
				else if (!se.would_block()) throw se;
			}
			//
			////////////////////////////////////

			// Send messages to log and echo to console
			log_stdout(cur_stdout, new_stdout);
		} while (1);
	/*} catch (exception &e) {
		cerr << "minecraft_server_loop(): Unexpected exception: " << e.what() << endl;
	} */

	if (mc_server) delete mc_server;
}

/******************************************************************************
 * List of messages
 *
 * K - Terminate the proxy server and minecraft server
 * M - Print [message] to console
 * R - Start/Restart minecraft server
 * S - Tell minecraft server to stop.
 * C - Issue a server command
 * P - Start a proxy tunnel
 * H - Get chat logs
 * L - Get sys logs
 * B - Run backup
 ******************************************************************************/

void process_http_cmd(char cmd, ChildProcess *http_server, ChildProcess *mc_server) {
	string reply;

	switch(cmd) {
	case 'L':
	case 'H': {
		int lc_pos;
		long rec;
		size_t lc;
		size_t datalen;

		string uri = http_server->out().read_line();
		stringstream ss;

		if (uri.compare("last") == 0) {
			int ret;

			if (cmd == 'L') ret = sys_log->last_rec();
			else ret = chat_log->last_rec();

			// This is insane... isn't there a better standards compliant way?
			ss << ret;
			string data(ss.str());

			/* int a = data.find_first_not_of(' ');
			int b = data.find_last_not_of(' ');
			data = data.substr(a, b - a + 1); */

			datalen = data.length();
			http_server->in().blocking_write((char *) &datalen, sizeof(size_t));
			http_server->in().blocking_write(data.c_str(), datalen);

		} else {
			lc_pos = uri.find("/", 1);
			if (lc_pos == string::npos) {
				rec = atoi(uri.substr(0, uri.length()).c_str());
				lc = 1;
			} else {
				lc = atoi(uri.substr(lc_pos + 1, uri.length() - lc_pos - 1).c_str());
				rec = atoi(uri.substr(0, lc_pos).c_str());
			}

			// safety...
			if (lc > 100) lc = 100;

			string ret;

			if (cmd == 'L') ret = sys_log->retrieve_logs(rec, &lc);
			else ret = chat_log->retrieve_logs(rec, &lc);

			ss << (rec + lc - 1) << "\n" << ret;
			ret = string(ss.str());

			datalen = ret.length();
			http_server->in().blocking_write((char *)&datalen, sizeof(size_t));
			http_server->in().blocking_write(ret.c_str(), datalen);
		}

		return;
	}

	case 'M':
		cout << "h" << http_server->out().read_line() << endl;
		return;

	case 'K':
		reply = "Termination command issued";
		terminating = true;
		if (mc_server) {
			mc_server->in().write_line("stop");
		}

		break;

	case 'B':
		if (mc_server) mc_server->in().write_line("stop");
		start_backup = true;
		reply = "Backup procedures initiated";
		break;

	case 'S':
		//cout << "hRemote stop message received" << endl;
		if (mc_server) {
			reply = "Stop command issued";
			mc_server->in().write_line("stop");
		} else reply = "Minecraft server isn't running. Can't stop it.";
		break;

	case 'R':
		if (mc_server) {
			mc_server->in().write_line("stop");
			reply = "Restart command issued";
		} else reply = "Start command issued";
		start_serv = true;
		break;

	case 'C': {
		string console_cmd = http_server->out().read_line();
		if (mc_server) {
			reply = "Issuing command: " + console_cmd;

            if (console_cmd.substr(0, 4).compare("say ") == 0) {
                // This was a console issued chat message
                cout << 'c' << console_cmd.substr(4, console_cmd.length()) << endl;
            }

			mc_server->in().write_line(console_cmd);
		} else reply = "Server isn't running";
		break;
	}

	default:
		reply = "Unknown message received!";
	}

	cout << "h" << reply << endl;
	http_server->in().write_line(reply);
}

#define RET_LEN	1024

void http_server_get_block_and_send(BasicHttpServer &http) {
	size_t len;
	cin.read((char *) &len, sizeof(size_t));
	char *buf = new char[len + 1];

	cin.read(buf, len);
	buf[len] = 0;

	http.set_data(string(buf));
	http.reply(BasicHttpServer::STATUS_OK, "OK");

	delete [] buf;
}

std::string load_page(char *start, char *end, BasicHttpServer &serv) {
	size_t page_len = end - start;
	string page(start, page_len);

	size_t pos = 0;

	while ((pos = page.find("$user", pos)) != string::npos) {
		page.replace(pos, 5, serv.get_auth_user());
	}

	return page;
}

void http_send_backup_list(BasicHttpServer & http)
{
    DIR *dp;
    struct dirent *dirp;
    vector<string> backups;

    if((dp  = opendir("backups")) == NULL) {
		http.set_data("Could not list contents of directory: Could not open directory");
		http.reply(BasicHttpServer::STATUS_OK, "OK");
	} else {

		ostringstream oss;

		while ((dirp = readdir(dp)) != NULL) {
			string dir = dirp->d_name;
			size_t pos = dir.find(".tar.bz2");
			if (pos == string::npos) continue;
			dir = dir.substr(0, pos);

			backups.push_back(dir);
		}

		closedir(dp);

		sort(backups.begin(), backups.end());

		for (vector<string>::iterator I = backups.begin(); I != backups.end(); ++I) {
			oss << "<a href=\"" << *I << "\">"
				<< *I << "</a><br />";
		}

		http.set_data(oss.str());
		http.reply(BasicHttpServer::STATUS_OK, "OK");
	}
}

void http_server_loop() {
	cout << "M\nStarting HTTP server on port " << HTTP_PORT << endl;

	ListenerSocket ls(HTTP_PORT, 10);

	if (geteuid() == 0) { // Hmm.... we are root...
		uid_t real_id = getuid();

		if (real_id == 0) {
			cout << "M\nCurrent effective user is root, and the real ID is also root. "
					"It is more secure to chown the binary to root, and flip the setuid "
					"bit. Then I can change uid to the real (unprivilaged) "
					"user." << endl;
		} else {
			cout << "M\nCurrent effective user is root. Changing effective uid to "
					<< real_id << "." << endl;
			seteuid(real_id);
		}
	}

	BasicHttpDigestAuthInfo authinfo("Minecraft Proxy");

	authinfo.HA1["tan_ce"] = "8b3d7b7eb44b9cebb515674e5e63edf8";
	authinfo.HA1["server"] = "1d160c5e0ead19842e717fec60c4cb13";

	char ret[RET_LEN];
	while(1) {
		ServerSocket *ss = ls.accept_connection();
		BasicHttpServer http(ss);

		try {
			http.enable_digest_auth(authinfo);
			http.add_noauth_path(baseurl + "proxy");
			http.add_noauth_path(baseurl + "jquery.js");
			http.add_noauth_path("favicon.ico");
			http.process_request();

			if (!http.active()) throw 0;

			string uri;

			// Normalize URI
			if (http.get_uri().length() >= baseurl.length() &&
					http.get_uri().substr(0, baseurl.length()).compare(baseurl) == 0) {
				uri = http.get_uri().substr(baseurl.length() - 1, http.get_uri().length() - baseurl.length() + 1);
			} else {
				http.set_location(baseurl);
				http.reply(BasicHttpServer::STATUS_MOVED_PERMANENTLY, "Look here");
				throw 0;
			}

			//cout << "M\nIncoming connection. URI = " << uri << endl;

			//if (http.get_req_type() == BasicHttpServer::REQ_POST) {
				if (http.get_uri().length() == baseurl.length()) {
					http.set_data(load_page(&_binary_html_index_html_start, &_binary_html_index_html_end, http));
					http.reply(BasicHttpServer::STATUS_OK, "OK");
					throw 0;

				} else if (uri.compare("/jquery.js") == 0) {
					http.set_data(load_page(&_binary_html_jquery_js_start, &_binary_html_jquery_js_end, http));
					http.set_content_type("application/x-javascript; charset=utf-8");
					http.reply(BasicHttpServer::STATUS_OK, "OK");
					throw 0;

				} else if (uri.compare("/download/backups/") == 0) {
					http_send_backup_list(http);
					throw 0;

				} else if (uri.substr(0, 18).compare("/download/backups/") == 0 && uri.length() > 18) {
					string backup_name = uri.substr(18, uri.length() - 18);
					if (backup_name.find(".") != string::npos) {
						http.reply(BasicHttpServer::STATUS_FORBIDDEN, "Bad filename");
						throw 0;
					}

					ifstream bf;
					bf.open((string("backups/") + backup_name + ".tar.bz2").c_str(), ofstream::in);

					if (bf.fail()) {
						bf.close();
						http.reply(BasicHttpServer::STATUS_NOT_FOUND, "Backup file not found");
						throw 0;
					}

					http.set_content_type("application/x-bzip-compressed-tar");
					http.set_header("Content-Disposition", string("attachment; filename=\"") + backup_name + ".tar.bz2\"");
					http.begin_chunked_transfer();

					char buf[1024];
					size_t size_read;
					do {
						bf.read(buf, 1024);
						size_read = bf.gcount();
						http.send_chunk(buf, size_read);
					} while (size_read > 0);

					throw 0;

				} else if (uri.compare("/backup") == 0) {
					cout << "B" << endl;

				} else if (uri.compare("/stop") == 0) {
					cout << "S" << endl;

				} else if (uri.substr(0, 9).compare("/shutdown") == 0) {
					cout << "K" << endl;

				} else if (uri.compare("/restart") == 0) {
					cout << "R" << endl;

				} else if (uri.substr(0, 5).compare("/cmd/") == 0) {
					string user_cmd = uri.substr(5, uri.length() - 5);
					for (int i = 0; i < user_cmd.length(); i++) {
						if (user_cmd[i] == '\n') user_cmd[i] = ' ';
					}
					cout << "C\n" << user_cmd << endl;

				} else if (uri.substr(0, 6).compare("/chat/") == 0) {
					cout << "H\n" << uri.substr(6, uri.length() - 6) << endl;

					http.set_content_type("text/plain");
					http_server_get_block_and_send(http);

					throw 0;

				} else if (uri.substr(0, 5).compare("/log/") == 0) {
					cout << "L\n" << uri.substr(5, uri.length() - 5) << endl;

					http.set_content_type("text/plain");
					http_server_get_block_and_send(http);

					throw 0;

				} else {
					http.set_data("Invalid Page Requested");
					http.reply(BasicHttpServer::STATUS_NOT_FOUND, "Not Found");
					throw 0;
				}

				cin.getline(ret, RET_LEN);

				http.set_data(ret);
				http.reply(BasicHttpServer::STATUS_OK, "OK");

			//} else { // GET

			//}
		} catch (SocketError &se) {
			// Silently ignore...
			ss->close_sock();
			cout << "M\nCaught a socket error: " << se.what() << endl;
		} catch (int) {
			// Quick escape
		}

		//cerr << "Session count: " << authinfo.session_count() << endl;
        delete ss;
	}
}
